package io.simple.netty;

import com.google.common.base.Preconditions;
import injections.annotations.PostInject;
import injections.annotations.Property;
import io.simple.core.RemoteRequestContext;
import io.simple.core.RequestContext;
import io.simple.core.chain.Pipeline;
import io.simple.signals.Signal;
import io.simple.signals.Signals;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.*;
import org.jboss.netty.handler.codec.http.*;
import org.jboss.netty.handler.timeout.ReadTimeoutHandler;
import org.jboss.netty.util.HashedWheelTimer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import simple.utils.concurrent.ConcurrentProperties;

import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;

/**
 * Created with IntelliJ IDEA.
 * User: rehak
 * Date: 8/20/13
 * Time: 10:41 AM
 * To change this template use File | Settings | File Templates.
 */
public class HTTPTransport extends AbstractNettyTransport<HttpRequest, HttpResponse> {

    private static final Logger LOGGER = LoggerFactory.getLogger(HTTPTransport.class);
    public static final String URI = "uri";
    public static final String METHOD = "method";
    public static final String VERSION = "version";
    public static final String HTTP_STATUS = "status";
    private static final int DEFAULT_STATUS = HttpResponseStatus.OK.getCode();
    @Property(optional = true)
    protected int maxRequestInitialLineLength = 8192;
    @Property(optional = true)
    protected int maxRequestHeaderSize = 8192;
    @Property(optional = true)
    protected int maxRequestChunkSize = 8192;
    @Property(optional = true)
    protected int maxContentLength = 5242880;
    @Property(optional = true)
    protected long timerTickDurationMillis = 10;
    @Property(optional = true)
    protected int timerTicksPerWheel = 1000;
    @Property(optional = true)
    protected long readTimeOutMillis = 5000;
    @Property(optional = true)
    private List<Pipeline> pipelines = new CopyOnWriteArrayList<>();
    protected HashedWheelTimer timer;

    public HTTPTransport() {
    }

    public HTTPTransport(int[] ports, String host, boolean keepAlive, int maxRequestInitialLineLength, int maxRequestHeaderSize, int maxRequestChunkSize, int maxContentLength, long timerTickDurationMillis, int timerTicksPerWheel, long readTimeOutMillis, Collection<Pipeline> pipelines) {
        super(ports, host, keepAlive);
        this.maxRequestInitialLineLength = maxRequestInitialLineLength;
        this.maxRequestHeaderSize = maxRequestHeaderSize;
        this.maxRequestChunkSize = maxRequestChunkSize;
        this.maxContentLength = maxContentLength;
        this.timerTickDurationMillis = timerTickDurationMillis;
        this.timerTicksPerWheel = timerTicksPerWheel;
        this.readTimeOutMillis = readTimeOutMillis;
        this.pipelines.addAll(pipelines);
        init();
    }

    @PostInject
    private void init() {
        // timer
        timer = new HashedWheelTimer(timerTickDurationMillis, TimeUnit.MILLISECONDS, timerTicksPerWheel);
    }

    public SimpleChannelHandler getRoutingHandler() {
        return new SimpleChannelHandler() {
            @Override
            public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
                // process incoming HTTP message
                RequestContext rc = createRequestContext((HttpRequest) e.getMessage(), e.getRemoteAddress());
                try {
                    // find suitable router
                    for (Pipeline r : pipelines) {
                        if (r.canProcess(rc.getRequestPath())) {
                            ByteBuffer res = r.process(rc, ((HttpRequest) e.getMessage()).getContent().toByteBuffer());
                            HttpResponse out = createOutput(rc, res);
                            ChannelFuture write = ctx.getChannel().write(out);
                            write.addListener(ChannelFutureListener.CLOSE);
                            return;
                        }
                    }
                    // if no suitable router found
                    LOGGER.debug("No router for " + rc.getRequestPath());
                    Signals.throwStat(Signals.BAD_REQUEST, "No suitable router found for request " + rc.getRequestPath() + " !");
                } catch (Signal signal) {
                    // process possible exception
                    ByteBuffer out = processSignal(signal, rc);
                    HttpResponse output = createOutput(rc, out);
                    ChannelFuture write = ctx.getChannel().write(output);
                    write.addListener(ChannelFutureListener.CLOSE);
                }
            }

            @Override
            public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception {
                try {
                    LOGGER.error("Exception caught!", e.getCause());
                    ctx.getChannel().close();
                } catch (Throwable t) {
                    LOGGER.error("Error processing error (Microsoft LOL)!", t);
                }
            }
        };
    }

    @Override
    public ChannelPipelineFactory getPipelineFactory() {
        return new ChannelPipelineFactory() {
            @Override
            public ChannelPipeline getPipeline() {
                ChannelPipeline pipeline = Channels.pipeline();
                pipeline.addLast("Timeout", new ReadTimeoutHandler(timer, readTimeOutMillis, TimeUnit.MILLISECONDS) {
                    @Override
                    protected void readTimedOut(ChannelHandlerContext ctx) throws Exception {
                        LOGGER.debug("Timeout for " + ctx.getChannel().getRemoteAddress());
                        ctx.getChannel().close();
                    }
                });
                pipeline.addLast("HttpRequestDecoder", new HttpRequestDecoder(maxRequestInitialLineLength, maxRequestHeaderSize, maxRequestChunkSize));
                pipeline.addLast("HttpChunkAggregator", new HttpChunkAggregator(maxContentLength));
                pipeline.addLast("HttpResponseEncoder", new HttpResponseEncoder());
//                pipeline.addLast("HttpDeflater", new HttpContentCompressor(2));
                pipeline.addLast("DefaultPipeline", getRoutingHandler());
                return pipeline;
            }
        };
    }

    public RequestContext createRequestContext(HttpRequest in, SocketAddress remote) {
        // content
        ChannelBuffer content = in.getContent();
        // request path
        String uri = in.getUri();
        // create context
        RemoteRequestContext ctx = new RemoteRequestContext(uri, System.currentTimeMillis(), null, (InetSocketAddress) remote);
        // non-headers
        HttpMethod method = in.getMethod();
        HttpVersion protocolVersion = in.getProtocolVersion();
        ctx.getProperties().set(URI, uri);
        ctx.getProperties().set(METHOD, method.getName());
        ctx.getProperties().set(VERSION, protocolVersion.getText());
        // default status is OK
        ctx.setProperty(HTTP_STATUS, DEFAULT_STATUS);
        // headers
        HttpHeaders headers = in.headers();
        for (Map.Entry<String, String> e : headers) {
            ctx.getRequestHeaders().add(e.getKey(), e.getValue());
        }
        return ctx;
    }

    public ByteBuffer processSignal(Signal s, RequestContext rc) {
        // empty headers
        rc.getResponseHeaders().empty();
        // add headers from signal
        ConcurrentProperties<String> headers = s.getResponseHeaders();
        Set<String> headerNames = headers.getPropNames();
        for (String hn : headerNames) {
            List<String> vals = headers.getValues(hn);
            for (String hv : vals) {
                rc.getResponseHeaders().add(hn, hv);
            }
        }
        // set return status
        rc.getProperties().set(HTTP_STATUS, s.getStatusCode());
        if (s.getStatusCode() == 400) {
            LOGGER.info("Transport processing signal " + s + ", code " + s.getStatusCode() + ", msg: " + s.getMsg());
        } else {
            LOGGER.info("Transport processing signal " + s + ", code " + s.getStatusCode() + ", msg: " + s.getMsg(), s);
        }
        String msg = "Unspecified error " + s.getStatusCode();
        if (s.getMsg() != null) {
            msg = s.getMsg();
        }
        try {
            return ByteBuffer.wrap(msg.getBytes("UTF-8"));
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("Unsuported UTF-8 lol..", e);
        }
    }

    public HttpResponse createOutput(RequestContext ctx, ByteBuffer data) {
        checkHas(VERSION, ctx.getProperties());
        checkHas(HTTP_STATUS, ctx.getProperties());
        String version = (String) ctx.getProperties().get(VERSION);
        int status = (int) ctx.getProperties().get(HTTP_STATUS);
        // version, status
        DefaultHttpResponse re = new DefaultHttpResponse(HttpVersion.valueOf(version), HttpResponseStatus.valueOf(status));
        // content
        if (data == null) {
            LOGGER.warn("Null response content!");
            data = ByteBuffer.wrap("".getBytes());
        }
        // content length header
        int size = data.limit();
        re.headers().set(HttpHeaders.Names.CONTENT_LENGTH, size);
        re.setContent(ChannelBuffers.wrappedBuffer(data));
        // ctx headers
        ConcurrentProperties<String> props = ctx.getResponseHeaders();
        Set<String> names = props.getPropNames();
        for (String headerName : names) {
            List<String> headerValues = props.getValues(headerName);
            for (String headerValue : headerValues) {
                re.headers().add(headerName, headerValue);
            }
        }
        // return
        return re;
    }

    private void checkHas(String propName, ConcurrentProperties props) {
        Preconditions.checkNotNull(props.get(propName), "Missing property " + propName);
    }

}
